﻿using System;
using System.IO;
using System.Windows;

namespace FirstFloor.Documents.IO
{
    /// <summary>
    /// A package reader for JPEG and PNG images.
    /// </summary>
    public class ImagePackageReader
        : MemoryPackageReader
    {
        private Stream imageStream;

        /// <summary>
        /// Initializes a new instance of the <see cref="ImagePackageReader"/> class.
        /// </summary>
        /// <param name="resourceUri">The resource URI.</param>
        public ImagePackageReader(Uri resourceUri)
        {
            if (resourceUri == null) {
                throw new ArgumentNullException("resourceUri");
            }
            var streamInfo = Application.GetResourceStream(resourceUri);
            if (streamInfo == null) {
                throw new ArgumentException(string.Format(Resources.ResourceNotFound, resourceUri));
            }
            Initialize(streamInfo.Stream);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ImagePackageReader"/> class.
        /// </summary>
        /// <param name="imageStream">The image stream.</param>
        public ImagePackageReader(Stream imageStream)
        {
            Initialize(imageStream);
        }

        private void Initialize(Stream imageStream)
        {
            if (!IsPngImage(imageStream) && !IsJpegImage(imageStream)) {
                throw new ArgumentException("Not a valid JPEG or PNG image");
            }
            this.imageStream = imageStream;
        }

        /// <summary>
        /// Gets the number of pages.
        /// </summary>
        /// <value>The page count.</value>
        public override int PageCount
        {
            get { return 1; }
        }

        private int ToInt32(byte[] data, int index)
        {
            return (int)(data[index] * 0x1000000 + data[index + 1] * 0x10000 + data[index + 2] * 0x100 + data[index + 3]);
        }

        private ushort ToUInt16(byte[] data, int index)
        {
            return (ushort)(data[index] * 0x100 + data[index + 1]);
        }

        private bool TryGetImageSize(Stream stream, out int width, out int height)
        {
            if (IsPngImage(stream) && stream.IsMatch(0, 0, 0, 13) && stream.IsMatch(73, 72, 68, 82)) {
                // reading IHDR chunk
                var size = stream.ReadBytes(8);

                width = ToInt32(size, 0);
                height = ToInt32(size, 4);

                return true;
            }

            if (IsJpegImage(stream)) {
                while (true) {
                    var header = stream.ReadBytes(4);
                    if (header[0] != 0xff) {
                        break;      // unknown marker
                    }

                    var marker = header[1];
                    if (marker >= 0xc0 && marker <= 0xcf && marker != 0xc4 && marker != 0xc8) {
                        byte[] imageSize = stream.ReadBytes(5);

                        width = ToUInt16(imageSize, 3);
                        height = ToUInt16(imageSize, 1);

                        return true;
                    }

                    // skip this marker
                    var size = ToUInt16(header, 2);

                    // seek to next marker
                    stream.Seek(size - 2, SeekOrigin.Current);
                }
            }


            // unable to determine image size
            width = 0;
            height = 0;

            return false;
        }

        /// <summary>
        /// Gets the XAML of specified page.
        /// </summary>
        /// <param name="pageNumber">The page number.</param>
        /// <returns>The page XAML.</returns>
        public override string GetPageXaml(int pageNumber)
        {
            if (pageNumber == 1){
                int width, height;

                if (!TryGetImageSize(this.imageStream, out width, out height)) {
                    throw new ArgumentException("Could not determine image width and height");
                }

                var xaml = @"
<doc:FixedPage Width='{0}' Height='{1}' xmlns:doc='http://schemas.firstfloorsoftware.com/documenttoolkit'>
  <Path Data='M0,0 {0},0 {0},{1} 0,{1}'>
    <Path.Fill>
      <ImageBrush ImageSource='image.png' />
    </Path.Fill>
  </Path>
</doc:FixedPage>";

                return string.Format(xaml, width, height);
            }

            return null;
        }

        /// <summary>
        /// Gets the part identified with given uri.
        /// </summary>
        /// <param name="partUri">The part URI.</param>
        /// <returns>The part stream.</returns>
        public override Stream GetPart(string partUri)
        {
            if (partUri == "image.png") {
                // must copy stream, it's closed by document toolkit
                this.imageStream.Seek(0, SeekOrigin.Begin);
                return this.imageStream.ToMemoryStream();
            }

            return null;        // not found
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public override void Dispose()
        {
            base.Dispose();

            if (this.imageStream != null) {
                this.imageStream.Dispose();
                this.imageStream = null;
            }
        }

        /// <summary>
        /// Determines whether the specified stream contains a PNG image.
        /// </summary>
        /// <param name="stream">The stream.</param>
        /// <returns>
        /// 	<c>true</c> if specified stream contains a PNG image; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsPngImage(Stream stream)
        {
            stream.VerifyReadAndSeek();
            stream.Seek(0, SeekOrigin.Begin);

            return stream.IsMatch(137, 80, 78, 71, 13, 10, 26, 10);
        }

        /// <summary>
        /// Determines whether the specified stream contains a JPEG image.
        /// </summary>
        /// <param name="stream">The stream.</param>
        /// <returns>
        /// 	<c>true</c> if specified stream contains an JPEG image; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsJpegImage(Stream stream)
        {
            stream.VerifyReadAndSeek();
            stream.Seek(0, SeekOrigin.Begin);

            return stream.IsMatch(0xff, 0xd8);
        }
    }
}
